#!/usr/bin/env python3
"""
Helper Utilities for Hyperliquid Trading Bot

This module provides utility functions for price calculations,
risk management, and other common trading operations.
"""

import asyncio
import logging
import math
import time
from decimal import Decimal, ROUND_DOWN, ROUND_UP
from typing import List, Dict, Optional, Tuple, Any, Union


class PriceCalculator:
    """
    Utility class for price and quantity calculations
    """
    
    @staticmethod
    def calculate_grid_levels(center_price: Decimal, 
                            grid_spread: Decimal, 
                            num_levels: int,
                            upper_bound: Optional[Decimal] = None,
                            lower_bound: Optional[Decimal] = None) -> Tuple[List[Decimal], List[Decimal]]:
        """
        Calculate grid trading price levels
        
        Args:
            center_price: Current market price
            grid_spread: Spread between grid levels (as percentage)
            num_levels: Number of levels on each side
            upper_bound: Maximum price bound
            lower_bound: Minimum price bound
            
        Returns:
            Tuple of (buy_levels, sell_levels)
        """
        buy_levels = []
        sell_levels = []
        
        for i in range(1, num_levels + 1):
            # Calculate buy level (below center price)
            buy_price = center_price * (Decimal("1") - grid_spread * i)
            if lower_bound is None or buy_price >= lower_bound:
                buy_levels.append(buy_price)
            
            # Calculate sell level (above center price)
            sell_price = center_price * (Decimal("1") + grid_spread * i)
            if upper_bound is None or sell_price <= upper_bound:
                sell_levels.append(sell_price)
        
        return buy_levels, sell_levels
    
    @staticmethod
    def calculate_position_size(account_balance: Decimal,
                              risk_percentage: Decimal,
                              entry_price: Decimal,
                              stop_loss_price: Optional[Decimal] = None) -> Decimal:
        """
        Calculate position size based on risk management
        
        Args:
            account_balance: Total account balance
            risk_percentage: Risk percentage per trade
            entry_price: Entry price
            stop_loss_price: Stop loss price (optional)
            
        Returns:
            Calculated position size
        """
        risk_amount = account_balance * risk_percentage
        
        if stop_loss_price:
            price_diff = abs(entry_price - stop_loss_price)
            if price_diff > 0:
                position_size = risk_amount / price_diff
            else:
                position_size = risk_amount / entry_price * Decimal("0.1")  # 10% default risk
        else:
            # Default to 10% of balance if no stop loss
            position_size = risk_amount / entry_price
        
        return position_size
    
    @staticmethod
    def round_to_tick_size(price: Decimal, tick_size: Decimal) -> Decimal:
        """
        Round price to exchange tick size
        
        Args:
            price: Price to round
            tick_size: Exchange tick size
            
        Returns:
            Rounded price
        """
        if tick_size <= 0:
            return price
        
        return (price / tick_size).quantize(Decimal("1"), rounding=ROUND_DOWN) * tick_size
    
    @staticmethod
    def round_to_lot_size(quantity: Decimal, lot_size: Decimal) -> Decimal:
        """
        Round quantity to exchange lot size
        
        Args:
            quantity: Quantity to round
            lot_size: Exchange lot size
            
        Returns:
            Rounded quantity
        """
        if lot_size <= 0:
            return quantity
        
        return (quantity / lot_size).quantize(Decimal("1"), rounding=ROUND_DOWN) * lot_size
    
    @staticmethod
    def calculate_percentage_change(old_price: Decimal, new_price: Decimal) -> Decimal:
        """
        Calculate percentage change between two prices
        
        Args:
            old_price: Original price
            new_price: New price
            
        Returns:
            Percentage change
        """
        if old_price == 0:
            return Decimal("0")
        
        return (new_price - old_price) / old_price * Decimal("100")
    
    @staticmethod
    def calculate_funding_pnl(position_size: Decimal,
                            funding_rate: Decimal,
                            mark_price: Decimal,
                            hours: int = 8) -> Decimal:
        """
        Calculate funding PnL for a position
        
        Args:
            position_size: Position size (positive for long, negative for short)
            funding_rate: Funding rate
            mark_price: Mark price
            hours: Funding period in hours
            
        Returns:
            Funding PnL
        """
        notional_value = abs(position_size) * mark_price
        funding_payment = notional_value * funding_rate * (Decimal(str(hours)) / Decimal("8"))
        
        # Long positions pay funding when rate is positive
        # Short positions receive funding when rate is positive
        if position_size > 0:  # Long position
            return -funding_payment
        else:  # Short position
            return funding_payment


class RiskManager:
    """
    Risk management utilities
    """
    
    def __init__(self, max_drawdown: Decimal = Decimal("0.1")):
        self.max_drawdown = max_drawdown
        self.peak_balance = Decimal("0")
        self.current_drawdown = Decimal("0")
        self._logger = logging.getLogger(__name__)
    
    def update_balance(self, current_balance: Decimal) -> bool:
        """
        Update balance and check drawdown
        
        Args:
            current_balance: Current account balance
            
        Returns:
            True if within risk limits, False if max drawdown exceeded
        """
        # Update peak balance
        if current_balance > self.peak_balance:
            self.peak_balance = current_balance
        
        # Calculate current drawdown
        if self.peak_balance > 0:
            self.current_drawdown = (self.peak_balance - current_balance) / self.peak_balance
        
        # Check if max drawdown exceeded
        if self.current_drawdown > self.max_drawdown:
            self._logger.warning(
                f"Max drawdown exceeded: {self.current_drawdown:.2%} > {self.max_drawdown:.2%}"
            )
            return False
        
        return True
    
    def calculate_max_position_size(self, 
                                  account_balance: Decimal,
                                  leverage: Decimal,
                                  risk_per_trade: Decimal) -> Decimal:
        """
        Calculate maximum position size based on risk parameters
        
        Args:
            account_balance: Account balance
            leverage: Maximum leverage
            risk_per_trade: Risk percentage per trade
            
        Returns:
            Maximum position size
        """
        max_risk_amount = account_balance * risk_per_trade
        max_position_value = account_balance * leverage
        
        # Use the more conservative limit
        return min(max_risk_amount, max_position_value)
    
    def should_reduce_position(self, 
                             current_position: Decimal,
                             unrealized_pnl: Decimal,
                             account_balance: Decimal) -> bool:
        """
        Check if position should be reduced due to risk
        
        Args:
            current_position: Current position size
            unrealized_pnl: Unrealized PnL
            account_balance: Account balance
            
        Returns:
            True if position should be reduced
        """
        # Check if unrealized loss is too large
        if unrealized_pnl < 0:
            loss_percentage = abs(unrealized_pnl) / account_balance
            if loss_percentage > self.max_drawdown / 2:  # 50% of max drawdown
                return True
        
        return False


class OrderManager:
    """
    Order management utilities
    """
    
    def __init__(self):
        self._pending_orders: Dict[str, Dict] = {}
        self._order_history: List[Dict] = []
        self._logger = logging.getLogger(__name__)
    
    def add_pending_order(self, order_id: str, order_data: Dict):
        """Add order to pending list"""
        self._pending_orders[order_id] = {
            **order_data,
            "timestamp": time.time(),
            "status": "pending"
        }
    
    def update_order_status(self, order_id: str, status: str, filled_amount: Decimal = Decimal("0")):
        """Update order status"""
        if order_id in self._pending_orders:
            self._pending_orders[order_id]["status"] = status
            self._pending_orders[order_id]["filled_amount"] = filled_amount
            
            if status in ["filled", "cancelled", "rejected"]:
                # Move to history
                self._order_history.append(self._pending_orders[order_id])
                del self._pending_orders[order_id]
    
    def get_pending_orders(self, symbol: Optional[str] = None) -> List[Dict]:
        """Get pending orders"""
        if symbol:
            return [order for order in self._pending_orders.values() 
                   if order.get("symbol") == symbol]
        return list(self._pending_orders.values())
    
    def get_order_history(self, limit: int = 100) -> List[Dict]:
        """Get order history"""
        return self._order_history[-limit:]
    
    def cleanup_old_orders(self, max_age_seconds: int = 3600):
        """Clean up old pending orders"""
        current_time = time.time()
        expired_orders = []
        
        for order_id, order_data in self._pending_orders.items():
            if current_time - order_data["timestamp"] > max_age_seconds:
                expired_orders.append(order_id)
        
        for order_id in expired_orders:
            self._logger.warning(f"Removing expired order: {order_id}")
            self.update_order_status(order_id, "expired")


class PerformanceTracker:
    """
    Performance tracking utilities
    """
    
    def __init__(self):
        self.trades: List[Dict] = []
        self.daily_pnl: Dict[str, Decimal] = {}
        self.start_balance: Optional[Decimal] = None
        self._logger = logging.getLogger(__name__)
    
    def add_trade(self, trade_data: Dict):
        """Add completed trade"""
        trade_data["timestamp"] = time.time()
        self.trades.append(trade_data)
        
        # Update daily PnL
        date_str = time.strftime("%Y-%m-%d", time.localtime())
        pnl = trade_data.get("pnl", Decimal("0"))
        
        if date_str not in self.daily_pnl:
            self.daily_pnl[date_str] = Decimal("0")
        
        self.daily_pnl[date_str] += pnl
    
    def calculate_statistics(self) -> Dict[str, Any]:
        """Calculate trading statistics"""
        if not self.trades:
            return {}
        
        total_trades = len(self.trades)
        winning_trades = [t for t in self.trades if t.get("pnl", 0) > 0]
        losing_trades = [t for t in self.trades if t.get("pnl", 0) < 0]
        
        win_rate = len(winning_trades) / total_trades if total_trades > 0 else 0
        
        total_pnl = sum(t.get("pnl", Decimal("0")) for t in self.trades)
        avg_win = sum(t.get("pnl", Decimal("0")) for t in winning_trades) / len(winning_trades) if winning_trades else Decimal("0")
        avg_loss = sum(t.get("pnl", Decimal("0")) for t in losing_trades) / len(losing_trades) if losing_trades else Decimal("0")
        
        profit_factor = abs(avg_win / avg_loss) if avg_loss != 0 else float('inf')
        
        return {
            "total_trades": total_trades,
            "winning_trades": len(winning_trades),
            "losing_trades": len(losing_trades),
            "win_rate": win_rate,
            "total_pnl": total_pnl,
            "average_win": avg_win,
            "average_loss": avg_loss,
            "profit_factor": profit_factor,
            "daily_pnl": dict(self.daily_pnl)
        }
    
    def get_sharpe_ratio(self, risk_free_rate: Decimal = Decimal("0.02")) -> Decimal:
        """Calculate Sharpe ratio"""
        if len(self.daily_pnl) < 2:
            return Decimal("0")
        
        daily_returns = list(self.daily_pnl.values())
        avg_return = sum(daily_returns) / len(daily_returns)
        
        # Calculate standard deviation
        variance = sum((r - avg_return) ** 2 for r in daily_returns) / len(daily_returns)
        std_dev = variance.sqrt() if variance > 0 else Decimal("1")
        
        # Annualized Sharpe ratio
        daily_risk_free = risk_free_rate / Decimal("365")
        sharpe = (avg_return - daily_risk_free) / std_dev * Decimal("365").sqrt()
        
        return sharpe


class ConfigValidator:
    """
    Configuration validation utilities
    """
    
    @staticmethod
    def validate_trading_pair(trading_pair: str) -> bool:
        """Validate trading pair format"""
        if not trading_pair or "-" not in trading_pair:
            return False
        
        parts = trading_pair.split("-")
        return len(parts) == 2 and all(part.isalpha() for part in parts)
    
    @staticmethod
    def validate_decimal_range(value: Decimal, min_val: Decimal, max_val: Decimal) -> bool:
        """Validate decimal value is within range"""
        return min_val <= value <= max_val
    
    @staticmethod
    def validate_api_credentials(api_key: str, api_secret: str) -> bool:
        """Validate API credentials format"""
        return bool(api_key and api_secret and len(api_key) > 10 and len(api_secret) > 10)


class AsyncRetry:
    """
    Async retry decorator utility
    """
    
    def __init__(self, max_retries: int = 3, delay: float = 1.0, backoff: float = 2.0):
        self.max_retries = max_retries
        self.delay = delay
        self.backoff = backoff
    
    def __call__(self, func):
        async def wrapper(*args, **kwargs):
            last_exception = None
            
            for attempt in range(self.max_retries + 1):
                try:
                    return await func(*args, **kwargs)
                except Exception as e:
                    last_exception = e
                    if attempt < self.max_retries:
                        wait_time = self.delay * (self.backoff ** attempt)
                        await asyncio.sleep(wait_time)
                    else:
                        raise last_exception
            
            raise last_exception
        
        return wrapper


# Utility functions

def format_decimal(value: Decimal, precision: int = 8) -> str:
    """Format decimal for display"""
    return f"{value:.{precision}f}".rstrip('0').rstrip('.')


def parse_decimal(value: Union[str, int, float]) -> Decimal:
    """Safely parse value to Decimal"""
    try:
        return Decimal(str(value))
    except (ValueError, TypeError):
        return Decimal("0")


def get_timestamp() -> int:
    """Get current timestamp in milliseconds"""
    return int(time.time() * 1000)


def format_timestamp(timestamp: int) -> str:
    """Format timestamp to readable string"""
    return time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(timestamp / 1000))